use darling::{FromDeriveInput, FromMeta};
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use proc_macro2::TokenStream as T2;
use syn::{parse_macro_input, DeriveInput};

use crate::periph::{rcc::{self, RccAxB}, steal_periph};

#[derive(Debug, FromDeriveInput)]
#[darling(attributes(dma))]
pub struct Config {
    pub init: String,

    pub dma: u8,
    pub stream: usize,
    pub ch: u8,

    #[darling(default)]
    pub priority: Priority,

    #[darling(default)]
    pub minc: bool,

    #[darling(default)]
    pub circ: bool,

    pub dir: Dir,
    
    #[darling(default)]
    pub tcie: bool,

    pub size: u8,
}

#[derive(Debug, FromMeta)]
pub enum Priority {
    #[darling(rename = "low")]
    Low,

    #[darling(rename = "middle")]
    Middle,

    #[darling(rename = "high")]
    High,

    #[darling(rename = "very_high")]
    VeryHigh,
}

impl Default for Priority {
    fn default() -> Self {
        Priority::Low
    }
}

#[derive(Debug, FromMeta)]
pub enum Dir {
    #[darling(rename = "periph_to_mem")]
    PeriphToMem,

    #[darling(rename = "mem_to_periph")]
    MemToPeriph,

    #[darling(rename = "mem_to_mem")]
    MemToMem,
}

impl Config {

    pub fn build(&self) -> T2 {
        
        let init_fn_rcc = format_ident!("{}_dma{}_s{}_rcc", self.init, self.dma, self.stream);
        let ndtr_fn = format_ident!("{}_dma{}_s{}_ndtr", self.init, self.dma, self.stream);
        let is_tc_fn = format_ident!("{}_dma{}_s{}_is_tc", self.init, self.dma, self.stream);
        let clear_flags_fn = format_ident!("{}_dma{}_s{}_clear_flags", self.init, self.dma, self.stream);

        let dmaen = format_ident!("dma{}en", self.dma);
        let rcc_init = rcc::rcc_enable(RccAxB::AHB1, &dmaen, false);
        let dma = format!("DMA{}", self.dma);
        let steal_dma = steal_periph(&dma);
        let fcr = if self.stream < 4 {
            format_ident!("lifcr")
        } else {
            format_ident!("hifcr")
        };
        let sr = if self.stream < 4 {
            format_ident!("lisr")
        } else {
            format_ident!("hisr")
        };

        let ctcif = format_ident!("ctcif{}", self.stream);
        let chtif = format_ident!("chtif{}", self.stream);
        let cteif = format_ident!("cteif{}", self.stream);
        let cdmeif = format_ident!("cdmeif{}", self.stream);
        let cfeif = format_ident!("cfeif{}", self.stream);
        let tcif = format_ident!("tcif{}", self.stream);

        let st = self.stream;
        let ch = self.ch;

        let pl = match self.priority {
            Priority::Low => format_ident!("low"),
            Priority::Middle => format_ident!("medium"),
            Priority::High => format_ident!("high"),
            Priority::VeryHigh => format_ident!("very_high")
        };

        let bits = format_ident!("bits{}", self.size);
        let minc = if self.minc {
            format_ident!("incremented")
        } else {
            format_ident!("fixed")
        };
        let circ = if self.circ {
            format_ident!("enabled")
        } else {
            format_ident!("disabled")
        };
        let dir = match self.dir {
            Dir::PeriphToMem => format_ident!("peripheral_to_memory"),
            Dir::MemToPeriph => format_ident!("memory_to_peripheral"),
            Dir::MemToMem => format_ident!("memory_to_memory"),
        };
        let tcie = if self.tcie {
            format_ident!("enabled")
        } else {
            format_ident!("disabled")
        };
        let init_fn = match self.dir {
            Dir::PeriphToMem => format_ident!("{}_dma{}_s{}_peripheral_to_memory", self.init, self.dma, self.stream),
            Dir::MemToPeriph => format_ident!("{}_dma{}_s{}_memory_to_peripheral", self.init, self.dma, self.stream),
            Dir::MemToMem => format_ident!("{}_dma{}_s{}_memory_to_memory", self.init, self.dma, self.stream),
        }; 
        
        quote! {

            #[inline(always)]
            pub fn #init_fn_rcc() {
                #rcc_init
            }

            #[inline(always)]
            pub fn #ndtr_fn() -> u16 {
                let dma = #steal_dma;
                let stream = dma.st(#st);
                stream.ndtr().read().ndt().bits()
            }

            #[inline(always)]
            pub fn #is_tc_fn() -> bool {
                let dma = #steal_dma;
                dma.#sr().read().#tcif().is_complete()
            }

            #[inline(always)]
            pub fn #clear_flags_fn() {
                let dma = #steal_dma;
                dma.#fcr().write(|w| 
                    w.#ctcif().clear()
                    .#chtif().clear()
                    .#cteif().clear()
                    .#cdmeif().clear()
                    .#cfeif().clear()
                );
            }

            #[inline(always)]
            pub fn #init_fn(par: u32, mar: u32, count: u16) {
                let dma = #steal_dma;
                dma.#fcr().write(|w| 
                    w.#ctcif().clear()
                    .#chtif().clear()
                    .#cteif().clear()
                    .#cdmeif().clear()
                    .#cfeif().clear()
                );

                let stream = dma.st(#st);
                stream.cr().reset();
                stream.ndtr().write(|w| w.ndt().set(count));
                stream.par().write(|w| unsafe { 
                    w.pa().bits(par)
                });
                stream.m0ar().write(|w| unsafe {
                    w.m0a().bits(mar)
                });

                stream.cr().write(|w| 
                    w.chsel().set(#ch)
                    .pl().#pl()
                    .msize().#bits()
                    .psize().#bits()
                    .minc().#minc()
                    .pinc().fixed()
                    .circ().#circ()
                    .dir().#dir()
                    .tcie().#tcie()
                    .en().enabled()
                );
            }
        }
    }
}

pub fn build(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let config = Config::from_derive_input(&input).unwrap();
    let build = config.build();
    let expand = quote! {
        impl #name {
            #build
        }
    };
    expand.into()
}
